/**
 * 
 */
package com.wkinney;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.zip.GZIPInputStream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;

import com.wkinney.util.FileUtil;



/**
 * @author williamkinney
 *
 */
public class SquidCacheParser {

    protected Logger logger = null;
    
    private boolean isVerboseMode;
    private boolean isDebugMode;
    private String cacheSourceDir;
    private String cacheDestinationDir;
    private String logFile = "squidCacheParser.log";
            
    private static final Character whiteSpaceChar = new Character((char) 8);

    private static final int BYTE_READ_BLOCK_SIZE = 64;
    private static final String HTTP_HEADER_GZIP = "Content-Encoding: gzip";
    
    // TODO move to properties file
    private static final String FILE_EXT_UNKNOWN = ".UNKNOWN";
    private static final String LOG4J_PATTERN = "%d{ISO8601} %-5p %c{1}: %m%n";
    
    public SquidCacheParser() {
        super();
    }
    
    
    public SquidCacheParser(String cacheSourceDir, String cacheDestinationDir) {
        super();
        this.cacheSourceDir = cacheSourceDir;
        this.cacheDestinationDir = cacheDestinationDir;
    }
    
    
    public SquidCacheParser(String cacheSourceDir, String cacheDestinationDir, boolean isVerboseMode, boolean isDebugMode, String logFile) {
        super();
        this.cacheSourceDir = cacheSourceDir;
        this.cacheDestinationDir = cacheDestinationDir;
        this.isDebugMode = isDebugMode;
        this.isVerboseMode = isVerboseMode;
        this.logFile = logFile;
    }
    
    public static void main (String[] args) {

        SquidCacheParser scp = new SquidCacheParser();
        
        scp.setUp(args);
        if (scp.getCacheSourceDir() != null && scp.getCacheDestinationDir() != null) {
            scp.execute();
        }
        

        
    }
    
    public String getCacheDestinationDir() {
        return cacheDestinationDir;
    }
    
    public String getCacheSourceDir() {
        return cacheSourceDir;
    }

    
    
    private void  writeByteListToDisk(List<ByteWrapper> bwList) throws IOException {
        
        DataInputStream dis = null;
        ByteArrayOutputStream baos = null;
      
        for(ByteWrapper byteWrapper : bwList) {

            
            try {
                
                
                baos = new ByteArrayOutputStream();
                
                
                dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(byteWrapper.getData())));
                
                // cache key == first 64 bytes
                dis.skipBytes(64);
                
                String line = dis.readLine();
                
                logger.debug("raw url: " + line);
                
                if (line.length() == 0) {
                    throw new RuntimeException("first line is null for cached object");
                }
                
                String url = null;
                
                int httpIndex = line.lastIndexOf("HTTP");
                int wsIndex = line.lastIndexOf(whiteSpaceChar);
                
               
                
                if (wsIndex != -1 && wsIndex < httpIndex) {
                    url = line.substring(0, wsIndex);
                } else if (httpIndex != -1) {
                    url = line.substring(0, httpIndex);
                } else {
                    throw new RuntimeException("could not find the end of the URL (no white space or HTTP header found in first line)");
                }
                
                // remove \r\n
                url = url.trim();
                
                logger.info("debug: " + url);
                                
                // handle dynamic or normal files. 
                // dynamic :
                // /dynjsfile.js?someAttr=dataCool&blah
                // OR
                //  file.jpg
                String fileExt = null;
                
                int fileExtIndex = url.lastIndexOf('.');
                int slashIndex = url.lastIndexOf('/');
                int fileDynamicPageIndex = url.indexOf('?');
                                        
                if (fileDynamicPageIndex != -1 && fileDynamicPageIndex < fileExtIndex) {
                    String tempUrl = url.substring(0, fileDynamicPageIndex);
                    
                    // e.g. /file? or /file.js?
                    tempUrl = tempUrl.substring(tempUrl.lastIndexOf('/'), fileDynamicPageIndex);
                                                
                    int dynamicFileExtIndex2 = tempUrl.lastIndexOf('.');
                    
                    if (dynamicFileExtIndex2 != -1) {
                        fileExt = tempUrl.substring(dynamicFileExtIndex2, tempUrl.length());
                    } else {
                        fileExt = FILE_EXT_UNKNOWN;
                    }
                } else if (fileExtIndex != -1 && fileExtIndex > slashIndex) {
                    fileExt = url.substring(fileExtIndex, url.length());
                } else {
                    fileExt = FILE_EXT_UNKNOWN;
                }
                
                int fileExtDynIndex = fileExt.indexOf('?');
                
                if (fileExtDynIndex != -1) {
                    fileExt = fileExt.substring(0, fileExtDynIndex);
                }
                
                logger.debug("fileExt: " + fileExt);
                
                boolean isGzipped = false;
                
                // skip http headers until reach empty line
                // check for gzip compression - Content-Encoding: gzip
                String l = dis.readLine();
                while (l.length() != 0) { 
                    if(l.toUpperCase().contains(HTTP_HEADER_GZIP.toUpperCase())) {
                        isGzipped = true;
                    }
                    l = dis.readLine();
                }
              
                
                // now begin reading cached data (the core data)
                
                byte[] b = null;
                int bsize = 0;
                
                while (true) {
                    
                    b = new byte[BYTE_READ_BLOCK_SIZE];
  
                    bsize = dis.read(b);
                    if (bsize == -1) {
                        logger.debug("breaking file read...");
                        break;
                    }
                    
                    baos.write(b);

                }
                
                final String outFileName = this.cacheDestinationDir + File.separator + byteWrapper.getFileName() + fileExt;
                
                
                if (isGzipped) {
                    // gzip decompress
                    
                    ByteArrayOutputStream unzippedbos = null;
                    GZIPInputStream gis = null;
                    try {
                        unzippedbos = new ByteArrayOutputStream();
                        gis = new GZIPInputStream(new ByteArrayInputStream(baos.toByteArray()));
                        
                        while (true) {
                            
                            b = new byte[BYTE_READ_BLOCK_SIZE];
          
                            bsize = gis.read(b);
                            if (bsize == -1) {
                                logger.debug("breaking gzipped data input stream...");
                                break;
                            }
                            
                            unzippedbos.write(b);

                        }
                        
                        logger.info("writing un-gzipped " +  baos.toByteArray().length + " bytes to " + outFileName);
                        
                        FileOutputStream fos = new FileOutputStream(outFileName);
                        unzippedbos.writeTo(fos);
                    } finally {
                        unzippedbos.close();
                        
                    }

                } else {
                    logger.info("writing " +  baos.toByteArray().length + " bytes to " + outFileName);
                    
                    FileOutputStream fos = new FileOutputStream(outFileName);
                    baos.writeTo(fos); 
                }
     
            } finally {
                baos.close();
                dis.close();
            }
                
            
        }
        
    }
    
    public void setUp(String[] args) {
        try {
            // create Options object
            Options options = new Options();

            Option help = new Option( "help", "print this message" );
            Option usage = new Option( "usage", "show usage information" );
            Option verbose = new Option( "verbose", "be extra verbose" );
            Option debug = new Option( "debug", "print debugging information" );
            Option logfile = OptionBuilder.withArgName("file").hasArg().withDescription("use given file for log").create("logfile");
            Option source = OptionBuilder.withArgName("dir").hasArg().withDescription("source directory for cached data").create("source");
            Option dest = OptionBuilder.withArgName("dir").hasArg().withDescription("destination directory for parsed cached data").create("dest");
            
            options.addOption(help);
            options.addOption( verbose );
            options.addOption( debug );
            options.addOption( logfile );
            options.addOption( source );
            options.addOption( dest );
            options.addOption( usage );
            // create the parser
            
            
            GnuParser parser = new GnuParser();
            // parse the command line arguments
            CommandLine line = parser.parse( options, args );

            if( line.getArgList().contains("help") ) {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printHelp( "SquidCacheParser", options );
                return;
            }
            if( line.getArgList().contains("usage") ) {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printUsage(new PrintWriter(System.out, true), 80, "SquidCacheParser -source <dir> -dest <dir>");
                return;
            }
            
            if( line.getArgList().isEmpty() && line.getOptions().length == 0 ) {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printHelp( "SquidCacheParser", options );
                return;
            }
            
            if( !line.hasOption( "source" ) && !line.hasOption( "dest" ) ) {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printUsage(new PrintWriter(System.out, true), 80, "SquidCacheParser -source <dir> -dest <dir>");
                return;
            }
            
            if( line.hasOption( "debug" ) ) {
                this.isDebugMode = true;
            }
            if( line.hasOption( "verbose" ) ) {
                this.isVerboseMode = true;
            }
            if( line.hasOption( "source" ) ) {
                this.cacheSourceDir = line.getOptionValue("source");
            }
            if( line.hasOption( "dest" ) ) {
                this.cacheDestinationDir = line.getOptionValue("dest");
            }
            if( line.hasOption( "logfile" ) ) {
                this.logFile = line.getOptionValue("logfile");
            }
           
        } catch (Exception e) {
            System.out.println( "Unexpected exception: " + e.getMessage() );
        }
    }
    
    public void execute() {
        try {
            configureLogging();
            
        } catch (Exception e) {
            System.out.println("Unexpected exception while configuring logging: " + e.getMessage() );
            return;
        }

        try {
            long start = System.currentTimeMillis();
            logger.info("Starting");
            executeForFileSource();
            logger.debug("total execution time in milliseconds: " + (System.currentTimeMillis() - start));
            logger.info("Complete");
        } catch(Exception e) {
            if (logger != null) {
                logger.error(ExceptionUtils.getStackTrace(e));
            } else {
                System.out.println(ExceptionUtils.getStackTrace(e));
            }
            
        }
       
        
        
 
        
    }
    
    private void configureLogging() throws IOException {
        
        Logger rootLogger = Logger.getRootLogger();

            
        if (this.isDebugMode) {
            rootLogger.setLevel(Level.DEBUG); 
        } else {
            rootLogger.setLevel(Level.INFO);  
        }
        
        if (this.isVerboseMode) {
            // PatternLayout.TTCC_CONVERSION_PATTERN
            rootLogger.addAppender(new ConsoleAppender(
                    new PatternLayout(LOG4J_PATTERN)));
        }
        this.logger = rootLogger.getLoggerRepository().getLogger(this.getClass().getPackage().getName());
        this.logger.addAppender(new FileAppender(new PatternLayout(LOG4J_PATTERN), this.logFile));
           
            
      
        
    }
    
    
    private void executeForFileSource() throws IOException {
        
       File sourceDir = new File(this.cacheSourceDir);
       
       if (!sourceDir.exists()) {
           throw new RuntimeException("supplied directory does not exist: " + this.cacheSourceDir);
       }   
       if (!sourceDir.isDirectory()) {
           throw new RuntimeException("supplied directory is not a directory: " + this.cacheSourceDir);
       }
       if (!sourceDir.canRead()) {
           throw new RuntimeException("cannot read supplied directory: " + this.cacheSourceDir);
       }      

        
       List<ByteWrapper> bwList = new ArrayList<ByteWrapper>(200);
        
       Collection<File> files = FileUtil.listFiles(sourceDir, null, true, true);
       
       byte[] b = null;
       int readResult = 0;
       
       BufferedInputStream bufIs = null;
       ByteArrayOutputStream bos = null;
       
       for (File file : files) {
           logger.info("cache file: " + file.getName());
                      
          
           
           try {
               bufIs = new BufferedInputStream(new FileInputStream(file));
               bos = new ByteArrayOutputStream();
               
               while (true) {
                   
                   b = new byte[BYTE_READ_BLOCK_SIZE];
                   readResult = bufIs.read(b);
                   if (readResult == -1) {
                       break;
                   }
                   
                   bos.write(b);
                   
               }
               bos.close();
               
               bwList.add(new ByteWrapper(file.getName(), bos.toByteArray()));
           } finally {
               if (bufIs != null) {
                   bufIs.close();
               }
           }

       }
       
       

       
       
       writeByteListToDisk(bwList);
  
       
    }
    

    

	
	
	
	
}
